﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Wintellect.LiveFx.Cloudboard
{
  /// <summary>
  /// A structure that keeps track of the positions along the <see cref="ItemCarousel"/>
  /// </summary>
  struct ItemWaypoint
  {
    public Double ScaleFactor;
    public Double X;
    public Double Y;
    public Double Width;
    public Double Height;
    public int ZIndex;
  }

  /// <summary>
  /// Interaction logic for ItemCarousel.xaml
  /// </summary>
  public partial class ItemCarousel : ContentControl
  {
    private const int InvalidIndex = -1;
    // TODO: Dependency property?
    private const int FrameWidth = 80;
    // TODO: Dependency property?
    private const int FrameHeight = FrameWidth;

    public static RoutedCommand RotateRight = new RoutedCommand();
    public static RoutedCommand RotateLeft = new RoutedCommand();
    
    private Storyboard[] animationStoryboards;
    private Storyboard previewStoryboard;
    private readonly System.Collections.ObjectModel.ObservableCollection<Object> items;
    private int currentIndex;
    private int previewingIndex = ItemCarousel.InvalidIndex;
    private bool layoutValid;

    // Using a DependencyProperty as the backing store for SelectedIndex.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedIndexProperty =
        DependencyProperty.Register("SelectedIndex", typeof(int), typeof(ItemCarousel), new UIPropertyMetadata(-1));

    public ItemCarousel()
    {
      InitializeComponent();
      this.items = new System.Collections.ObjectModel.ObservableCollection<Object>();
      this.items.CollectionChanged += (o, e) => {
        if (this.SelectedIndex >= this.items.Count)
        {
          this.SelectedIndex = this.items.Count - 1;
          this.currentIndex = this.SelectedIndex;
        }
        this.layoutValid = false; 
        this.InvalidateVisual(); 
      };
      this.SizeChanged += new SizeChangedEventHandler(ItemCarousel_SizeChanged);
    }

    public IList<Object> Items
    {
      get
      {
        return this.items;
      }
    }

    public int SelectedIndex
    {
      get { return (int)GetValue(SelectedIndexProperty); }
      set
      {
        SetValue(SelectedIndexProperty, value);
        this.RotateToSelected();
      }
    }

    public Object SelectedItem
    {
      get
      {
        Object output = null;
        if (this.SelectedIndex != -1 && this.items.Count > this.SelectedIndex)
        {
          output = this.items[this.SelectedIndex];
        }
        return output;
      }
    }

    void RotateRightCommandExecuted(Object sender, ExecutedRoutedEventArgs e)
    {
      this.SelectedIndex = (this.items.Count + this.SelectedIndex - 1) % this.items.Count;
      e.Handled = true;
    }

    void RotateLeftCommandExecuted(Object sender, ExecutedRoutedEventArgs e)
    {
      this.SelectedIndex = (this.SelectedIndex + 1) % this.items.Count;
      e.Handled = true;
    }

    /// <summary>
    /// Rotates the carousel to the item at <see cref="SelectedIndex"/>
    /// </summary>
    private void RotateToSelected()
    {
      if (this.SelectedIndex == -1 || this.SelectedIndex == this.currentIndex) return;

      int offset = MoveToCorrectPosition();
      this.currentIndex = (this.items.Count + this.currentIndex + offset) % this.items.Count;
    }

    private int MoveToCorrectPosition()
    {
      int numSteps = (this.items.Count + this.SelectedIndex - this.currentIndex) % this.items.Count;
      if (Math.Abs(numSteps) > this.items.Count / 2)
      {
        numSteps = numSteps - this.items.Count;
      }
      int offset = numSteps != 0 ? numSteps / Math.Abs(numSteps) : 0;
      System.Diagnostics.Debug.WriteLine(String.Format("Direction = {0}, SelectedIndex = {1}, CurrentIndex = {2}", offset > 0 ? "Clockwise" : "Counterclockwise", this.SelectedIndex, this.currentIndex));

      Storyboard allMovements = new Storyboard();
      for (int index = 0; index < this.items.Count; index++)
      {
        Storyboard b;
        if (offset > 0)
        {
          b = this.animationStoryboards[(this.items.Count - this.currentIndex + index - 1) % this.items.Count];
        }
        else if (offset == 0)
        {
          b = this.animationStoryboards[(this.items.Count - this.currentIndex + index) % this.items.Count];
        }
        else
        {
          b = this.animationStoryboards[(this.items.Count - this.currentIndex + index + 1) % this.items.Count];
        }
        Storyboard.SetTarget(b, (FrameworkElement)background.Children[index]);
        allMovements.Children.Add(b);
      }
      allMovements.Completed += (o, e) =>
      {
        if (this.currentIndex != this.SelectedIndex)
        {
          this.RotateToSelected();
        }
      };
      allMovements.Begin();
      return offset;
    }

    void ItemCarousel_SizeChanged(object sender, SizeChangedEventArgs e)
    {
      this.InvalidateVisual();
    }

    private void BuildLayout()
    {
      this.background.Children.Clear();

      this.currentIndex = 0;
      this.SelectedIndex = 0;

      IList<ItemWaypoint> waypoints = this.GenerateWaypoints();

      this.animationStoryboards = new Storyboard[this.items.Count];
      for (int index = 0; index < this.items.Count; index++)
      {
        ItemWaypoint waypoint = waypoints[index];
        ItemWaypoint nextWaypoint = waypoints[(index + 1) % waypoints.Count];

        // Create the rectangle that acts as the "frame" for the content
        Grid itemCanvas = new Grid();
        itemCanvas.Width = waypoint.Width;
        itemCanvas.Height = waypoint.Height;
        itemCanvas.RenderTransform = new ScaleTransform(waypoint.ScaleFactor, waypoint.ScaleFactor);
        itemCanvas.SetValue(Canvas.TopProperty, waypoint.Y);
        itemCanvas.SetValue(Canvas.LeftProperty, waypoint.X);
        itemCanvas.SetValue(Canvas.ZIndexProperty, waypoint.ZIndex);
        itemCanvas.Tag = index;
        itemCanvas.MouseLeftButtonDown += (o, e) =>
        {
          itemCanvas.CaptureMouse();
        };
        itemCanvas.MouseLeftButtonUp += (o, e) =>
        {
          if (itemCanvas.IsMouseCaptured) itemCanvas.ReleaseMouseCapture();

          if (itemCanvas.IsMouseOver)
          {
            //this.SelectedIndex = (int)itemCanvas.Tag;
            if (this.previewingIndex != ItemCarousel.InvalidIndex)
            {
              // If we had been previewing, push this item back where it's supposed to be
              this.MoveToCorrectPosition();
            }
            int tag = (int)itemCanvas.Tag;
            if (this.previewingIndex != tag)
            {
              this.previewingIndex = tag;
              this.previewStoryboard.Begin(itemCanvas);
            }
            else
            {
              this.previewingIndex = ItemCarousel.InvalidIndex;
            }
          }
        };

        this.GeneratePreviewStoryboard();

        Rectangle canvasBackground = new Rectangle();
        canvasBackground.HorizontalAlignment = HorizontalAlignment.Stretch;
        canvasBackground.VerticalAlignment = VerticalAlignment.Stretch;
        canvasBackground.Fill = new SolidColorBrush(Colors.White);
        canvasBackground.Stroke = new SolidColorBrush(Colors.DarkGray);
        canvasBackground.StrokeThickness = 2;
        canvasBackground.SetValue(Canvas.ZIndexProperty, -1);
        itemCanvas.Children.Add(canvasBackground);

        // TODO: We need a factory for this instead of this kludgy if block.
        if (null != this.items[index])
        {
          UIElement content;

          System.Windows.Media.ImageSource image = this.items[index] as System.Windows.Media.ImageSource;
          if (image != null)
          {
            Image imageContent = new Image();
            imageContent.Source = image;
            content = imageContent;
          }
          else
          {
            String textualContent = this.items[index].ToString();
            RichTextBox rtb = new RichTextBox();
            // Don't allow the user to edit the contents (at least, not in the carousel...)
            rtb.Focusable = false;
            rtb.LayoutTransform = new ScaleTransform(.333, .333);
            TextRange range = new TextRange(rtb.Document.ContentStart, rtb.Document.ContentEnd);
            if (MeshHelper.IsTextRTF(textualContent))
            {
              using (System.IO.Stream memoryStream = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(textualContent)))
              {
                range.Load(
                  memoryStream,
                  System.Windows.DataFormats.Rtf);
              }
            }
            else
            {
              range.Text = textualContent;
            }
              content = rtb;
          }

          itemCanvas.Children.Add(content);
        }

        Storyboard b = GenerateAnimationForWaypoint(nextWaypoint);
        this.animationStoryboards[(this.items.Count + index + 1) % this.items.Count] = b;

        this.background.Children.Add(itemCanvas);
      }
    }

    private IList<ItemWaypoint> GenerateWaypoints()
    {
      double midY = this.ActualHeight * .8 / 2;
      double midX = this.ActualWidth / 2;
      double swingX = midX * .8;
      double swingY = midY * .4;

      IList<ItemWaypoint> waypoints = new List<ItemWaypoint>();

      double angleIncrement = (Math.PI * 2) / this.items.Count;
      double angle = 0;
      for (int index = 0; index < this.items.Count; index++)
      {
        ItemWaypoint waypoint = new ItemWaypoint();

        if (index != 0)
        {
          waypoint.ScaleFactor = (3 + Math.Cos(angle)) / 5D;
        }
        else
        {
          waypoint.ScaleFactor = 2;
        }

        waypoint.Height = ItemCarousel.FrameHeight;
        waypoint.Width = ItemCarousel.FrameWidth;

        // Calculate the "left" for this waypoint
        waypoint.X = midX + (Math.Sin(angle) * swingX) - (waypoint.ScaleFactor * waypoint.Width) / 2;
        // Calculate the "top" for this waypoint
        waypoint.Y = midY + (Math.Cos(angle) * swingY) - (waypoint.ScaleFactor * waypoint.Height) / 2;
        // Find out where the item should be in the z-order
        waypoint.ZIndex = (int)(100 * Math.Cos(angle));

        waypoints.Add(waypoint);
        angle += angleIncrement;
      }
      return waypoints;
    }

    private static Storyboard GenerateAnimationForWaypoint(ItemWaypoint nextWaypoint)
    {
      TimeSpan duration = new TimeSpan(0, 0, 0, 0, 250);

      Storyboard b = new Storyboard();
      DoubleAnimation scaleXAnimation = new DoubleAnimation();
      Storyboard.SetTargetProperty(scaleXAnimation, new PropertyPath("(UIElement.RenderTransform).(ScaleTransform.ScaleX)"));
      scaleXAnimation.To = nextWaypoint.ScaleFactor;
      scaleXAnimation.Duration = duration;
      b.Children.Add(scaleXAnimation);

      DoubleAnimation scaleYAnimation = new DoubleAnimation();
      Storyboard.SetTargetProperty(scaleYAnimation, new PropertyPath("(UIElement.RenderTransform).(ScaleTransform.ScaleY)"));
      scaleYAnimation.To = nextWaypoint.ScaleFactor;
      scaleYAnimation.Duration = duration;
      b.Children.Add(scaleYAnimation);

      DoubleAnimation xAnimation = new DoubleAnimation();
      Storyboard.SetTargetProperty(xAnimation, new PropertyPath("(Canvas.Left)"));
      xAnimation.To = nextWaypoint.X;
      xAnimation.Duration = duration;
      b.Children.Add(xAnimation);

      DoubleAnimation yAnimation = new DoubleAnimation();
      Storyboard.SetTargetProperty(yAnimation, new PropertyPath("(Canvas.Top)"));
      yAnimation.To = nextWaypoint.Y;
      yAnimation.Duration = duration;
      b.Children.Add(yAnimation);

      Int32Animation zIndexAnimation = new Int32Animation();
      Storyboard.SetTargetProperty(zIndexAnimation, new PropertyPath("(Canvas.ZIndex)"));
      zIndexAnimation.To = nextWaypoint.ZIndex;
      zIndexAnimation.Duration = duration;
      b.Children.Add(zIndexAnimation);

      return b;
    }

    private void GeneratePreviewStoryboard()
    {
      Double previewScaleFactor = 3.0D;
      TimeSpan duration = new TimeSpan(0, 0, 0, 0, 250);

      this.previewStoryboard = new Storyboard();
      DoubleAnimation scaleXAnimation = new DoubleAnimation();
      Storyboard.SetTargetProperty(scaleXAnimation, new PropertyPath("(UIElement.RenderTransform).(ScaleTransform.ScaleX)"));
      scaleXAnimation.To = previewScaleFactor;
      scaleXAnimation.Duration = duration;
      this.previewStoryboard.Children.Add(scaleXAnimation);

      DoubleAnimation scaleYAnimation = new DoubleAnimation();
      Storyboard.SetTargetProperty(scaleYAnimation, new PropertyPath("(UIElement.RenderTransform).(ScaleTransform.ScaleY)"));
      scaleYAnimation.To = previewScaleFactor;
      scaleYAnimation.Duration = duration;
      this.previewStoryboard.Children.Add(scaleYAnimation);

      DoubleAnimation xAnimation = new DoubleAnimation();
      Storyboard.SetTargetProperty(xAnimation, new PropertyPath("(Canvas.Left)"));
      xAnimation.To = (this.ActualWidth - ItemCarousel.FrameWidth * previewScaleFactor) / 2;
      xAnimation.Duration = duration;
      this.previewStoryboard.Children.Add(xAnimation);

      DoubleAnimation yAnimation = new DoubleAnimation();
      Storyboard.SetTargetProperty(yAnimation, new PropertyPath("(Canvas.Top)"));
      yAnimation.To = (this.ActualHeight - ItemCarousel.FrameHeight * previewScaleFactor) / 2;
      yAnimation.Duration = duration;
      this.previewStoryboard.Children.Add(yAnimation);

      Int32Animation zIndexAnimation = new Int32Animation();
      Storyboard.SetTargetProperty(zIndexAnimation, new PropertyPath("(Canvas.ZIndex)"));
      zIndexAnimation.To = 101;
      zIndexAnimation.Duration = duration;
      this.previewStoryboard.Children.Add(zIndexAnimation);
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
      if (!this.layoutValid) this.BuildLayout();
      base.OnRender(drawingContext);
    }
  }
}
